Go 每日一库之 net/http(基础和中间件) 您所在的位置:网站首页 中间件 net Go 每日一库之 net/http(基础和中间件)

Go 每日一库之 net/http(基础和中间件)

2023-03-12 19:36| 来源: 网络整理| 查看: 265

Go 每日一库之 net/http(基础和中间件) 2021-07-13 Go 约 6152 字 预计阅读 13 分钟 | 阅读 文章目录 简介

几乎所有的编程语言都以Hello World作为入门程序的示例,其中有一部分以编写一个 Web 服务器作为实战案例的开始。每种编程语言都有很多用于编写 Web 服务器的库,或以标准库,或通过第三方库的方式提供。Go 语言也不例外。本文及后续的文章就去探索 Go 语言中的各个Web 编程框架,它们的基本使用,阅读它们的源码,比较它们优缺点。让我们先从 Go 语言的标准库net/http开始。标准库net/http让编写 Web 服务器的工作变得非常简单。我们一起探索如何使用net/http库实现一些常见的功能或模块,了解这些对我们学习其他的库或框架将会很有帮助。

Hello World

使用net/http编写一个简单的 Web 服务器非常简单:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 package main import ( "fmt" "net/http" ) func index(w http.ResponseWriter, r *http.Request) { fmt.Fprintln(w, "Hello World") } func main() { http.HandleFunc("/", index) http.ListenAndServe(":8080", nil) }

首先,我们调用http.HandleFunc("/", index)注册路径处理函数,这里将路径/的处理函数设置为index。处理函数的类型必须是:

1 func (http.ResponseWriter, *http.Request)

其中*http.Request表示 HTTP 请求对象,该对象包含请求的所有信息,如 URL、首部、表单内容、请求的其他内容等。

http.ResponseWriter是一个接口类型:

1 2 3 4 5 6 // net/http/server.go type ResponseWriter interface { Header() Header Write([]byte) (int, error) WriteHeader(statusCode int) }

用于向客户端发送响应,实现了ResponseWriter接口的类型显然也实现了io.Writer接口。所以在处理函数index中,可以调用fmt.Fprintln()向ResponseWriter写入响应信息。

仔细阅读net/http包中HandleFunc()函数的源码:

1 2 3 func HandleFunc(pattern string, handler func(ResponseWriter, *Request)) { DefaultServeMux.HandleFunc(pattern, handler) }

我们发现它直接调用了一个名为DefaultServeMux对象的HandleFunc()方法。DefaultServeMux是ServeMux类型的实例:

1 2 3 4 5 6 7 8 9 type ServeMux struct { mu sync.RWMutex m map[string]muxEntry es []muxEntry // slice of entries sorted from longest to shortest. hosts bool // whether any patterns contain hostnames } var DefaultServeMux = &defaultServeMux var defaultServeMux ServeMux

像这种提供默认类型实例的用法在 Go 语言的各个库中非常常见,在默认参数就已经足够的场景中使用默认实现很方便。ServeMux保存了注册的所有路径和处理函数的对应关系。ServeMux.HandleFunc()方法如下:

1 2 3 func (mux *ServeMux) HandleFunc(pattern string, handler func(ResponseWriter, *Request)) { mux.Handle(pattern, HandlerFunc(handler)) }

这里将处理函数handler转为HandlerFunc类型,然后调用ServeMux.Handle()方法注册。注意这里的HandlerFunc(handler)是类型转换,而非函数调用,类型HandlerFunc的定义如下:

1 2 3 4 5 type HandlerFunc func(ResponseWriter, *Request) func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) { f(w, r) }

HandlerFunc实际上是以函数类型func(ResponseWriter, *Request)为底层类型,为HandlerFunc类型定义了方法ServeHTTP。是的,Go 语言允许为(基于)函数的类型定义方法。Serve.Handle()方法只接受类型为接口Handler的参数:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 type Handler interface { ServeHTTP(ResponseWriter, *Request) } func (mux *ServeMux) Handle(pattern string, handler Handler) { if mux.m == nil { mux.m = make(map[string]muxEntry) } e := muxEntry{h: handler, pattern: pattern} if pattern[len(pattern)-1] == '/' { mux.es = appendSorted(mux.es, e) } mux.m[pattern] = e }

显然HandlerFunc实现了接口Handler。HandlerFunc类型只是为了方便注册函数类型的处理器。我们当然可以直接定义一个实现Handler接口的类型,然后注册该类型的实例:

1 2 3 4 5 6 7 type greeting string func (g greeting) ServeHTTP(w http.ResponseWriter, r *http.Request) { fmt.Fprintln(w, g) } http.Handle("/greeting", greeting("Welcome, dj"))

我们基于string类型定义了一个新类型greeting,然后为它定义一个方法ServeHTTP()(实现接口Handler),最后调用http.Handle()方法注册该处理器。

为了便于区分,我们将通过HandleFunc()注册的称为处理函数,将通过Handle()注册的称为处理器。通过上面的源码分析不难看出,它们在底层本质上是一回事。

注册了处理逻辑后,调用http.ListenAndServe(":8080", nil)监听本地计算机的 8080 端口,开始处理请求。下面看源码的处理:

1 2 3 4 func ListenAndServe(addr string, handler Handler) error { server := &Server{Addr: addr, Handler: handler} return server.ListenAndServe() }

ListenAndServe创建了一个Server类型的对象:

1 2 3 4 5 6 7 8 9 type Server struct { Addr string Handler Handler TLSConfig *tls.Config ReadTimeout time.Duration ReadHeaderTimeout time.Duration WriteTimeout time.Duration IdleTimeout time.Duration }

Server结构体有比较多的字段,我们可以使用这些字段来调节 Web 服务器的参数,如上面的ReadTimeout/ReadHeaderTimeout/WriteTimeout/IdleTimeout用于控制读写和空闲超时。在该方法中,先调用net.Listen()监听端口,将返回的net.Listener作为参数调用Server.Serve()方法:

1 2 3 4 5 6 7 8 func (srv *Server) ListenAndServe() error { addr := srv.Addr ln, err := net.Listen("tcp", addr) if err != nil { return err } return srv.Serve(ln) }

在Server.Serve()方法中,使用一个无限的for循环,不停地调用Listener.Accept()方法接受新连接,开启新 goroutine 处理新连接:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 func (srv *Server) Serve(l net.Listener) error { var tempDelay time.Duration // how long to sleep on accept failure for { rw, err := l.Accept() if err != nil { if ne, ok := err.(net.Error); ok && ne.Temporary() { if tempDelay == 0 { tempDelay = 5 * time.Millisecond } else { tempDelay *= 2 } if max := 1 * time.Second; tempDelay > max { tempDelay = max } srv.logf("http: Accept error: %v; retrying in %v", err, tempDelay) time.Sleep(tempDelay) continue } return err } tempDelay = 0 c := srv.newConn(rw) go c.serve(connCtx) } }

这里有一个指数退避策略的用法。如果l.Accept()调用返回错误,我们判断该错误是不是临时性地(ne.Temporary())。如果是临时性错误,Sleep一小段时间后重试,每发生一次临时性错误,Sleep的时间翻倍,最多Sleep 1s。获得新连接后,将其封装成一个conn对象(srv.newConn(rw)),创建一个 goroutine 运行其serve()方法。省略无关逻辑的代码如下:

1 2 3 4 5 6 7 func (c *conn) serve(ctx context.Context) { for { w, err := c.readRequest(ctx) serverHandler{c.server}.ServeHTTP(w, w.req) w.finishRequest() } }

serve()方法其实就是不停地读取客户端发送地请求,创建serverHandler对象调用其ServeHTTP()方法去处理请求,然后做一些清理工作。serverHandler只是一个中间的辅助结构,代码如下:

1 2 3 4 5 6 7 8 9 10 11 type serverHandler struct { srv *Server } func (sh serverHandler) ServeHTTP(rw ResponseWriter, req *Request) { handler := sh.srv.Handler if handler == nil { handler = DefaultServeMux } handler.ServeHTTP(rw, req) }

从Server对象中获取Handler,这个Handler就是调用http.ListenAndServe()时传入的第二个参数。在Hello World的示例代码中,我们传入了nil。所以这里handler会取默认值DefaultServeMux。调用DefaultServeMux.ServeHTTP()方法处理请求:

1 2 3 4 func (mux *ServeMux) ServeHTTP(w ResponseWriter, r *Request) { h, _ := mux.Handler(r) h.ServeHTTP(w, r) }

mux.Handler(r)通过请求的路径信息查找处理器,然后调用处理器的ServeHTTP()方法处理请求:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 func (mux *ServeMux) Handler(r *Request) (h Handler, pattern string) { host := stripHostPort(r.Host) return mux.handler(host, r.URL.Path) } func (mux *ServeMux) handler(host, path string) (h Handler, pattern string) { h, pattern = mux.match(path) return } func (mux *ServeMux) match(path string) (h Handler, pattern string) { v, ok := mux.m[path] if ok { return v.h, v.pattern } for _, e := range mux.es { if strings.HasPrefix(path, e.pattern) { return e.h, e.pattern } } return nil, "" }

上面的代码省略了大量的无关代码,在match方法中,首先会检查路径是否精确匹配mux.m[path]。如果不能精确匹配,后面的for循环会匹配路径的最长前缀。只要注册了/根路径处理,所有未匹配到的路径最终都会交给/路径处理。为了保证最长前缀优先,在注册时,会对路径进行排序。所以mux.es中存放的是按路径排序的处理列表:

1 2 3 4 5 6 7 8 9 10 11 12 13 func appendSorted(es []muxEntry, e muxEntry) []muxEntry { n := len(es) i := sort.Search(n, func(i int) bool { return len(es[i].pattern) = 0; i-- { handler = middlewares[i](handler) } return handler }

注意应用顺序是从右到左的,即右结合,越靠近原处理器的越晚执行。

利用帮助函数,注册可以简化为:

1 2 3 4 5 6 7 middlewares := []Middleware{ PanicRecover, WithLogger, Metric, } mux.Handle("/", applyMiddlewares(http.HandlerFunc(index), middlewares...)) mux.Handle("/greeting", applyMiddlewares(greeting("welcome, dj"), middlewares...))

上面每次注册处理逻辑都需要调用一次applyMiddlewares()函数,还是略显繁琐。我们可以这样来优化,封装一个自己的ServeMux结构,然后定义一个方法Use()将中间件保存下来,重写Handle/HandleFunc将传入的http.HandlerFunc/http.Handler处理器包装中间件之后再传给底层的ServeMux.Handle()方法:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 type MyMux struct { *http.ServeMux middlewares []Middleware } func NewMyMux() *MyMux { return &MyMux{ ServeMux: http.NewServeMux(), } } func (m *MyMux) Use(middlewares ...Middleware) { m.middlewares = append(m.middlewares, middlewares...) } func (m *MyMux) Handle(pattern string, handler http.Handler) { handler = applyMiddlewares(handler, m.middlewares...) m.ServeMux.Handle(pattern, handler) } func (m *MyMux) HandleFunc(pattern string, handler http.HandlerFunc) { newHandler := applyMiddlewares(handler, m.middlewares...) m.ServeMux.Handle(pattern, newHandler) }

注册时只需要创建MyMux对象,调用其Use()方法传入要应用的中间件即可:

1 2 3 4 5 6 7 8 9 middlewares := []Middleware{ PanicRecover, WithLogger, Metric, } mux := NewMyMux() mux.Use(middlewares...) mux.HandleFunc("/", index) mux.Handle("/greeting", greeting("welcome, dj"))

这种方式简单易用,但是也有它的问题,最大的问题是必须先设置好中间件,然后才能调用Handle/HandleFunc注册,后添加的中间件不会对之前注册的处理器/函数生效。

为了解决这个问题,我们可以改写ServeHTTP方法,在确定了处理器之后再应用中间件。这样后续添加的中间件也能生效。很多第三方库都是采用这种方式。http.ServeMux默认的ServeHTTP()方法如下:

1 2 3 4 5 6 7 8 9 10 11 func (m *ServeMux) ServeHTTP(w http.ResponseWriter, r *http.Request) { if r.RequestURI == "*" { if r.ProtoAtLeast(1, 1) { w.Header().Set("Connection", "close") } w.WriteHeader(http.StatusBadRequest) return } h, _ := m.Handler(r) h.ServeHTTP(w, r) }

改造这个方法定义MyMux类型的ServeHTTP()方法也很简单,只需要在m.Handler(r)获取处理器之后,应用当前的中间件即可:

1 2 3 4 5 6 7 func (m *MyMux) ServeHTTP(w http.ResponseWriter, r *http.Request) { // ... h, _ := m.Handler(r) // 只需要加这一行即可 h = applyMiddlewares(h, m.middlewares...) h.ServeHTTP(w, r) }

后面我们分析其他 Web 框架的源码时会发现,很多都是类似的做法。为了测试宕机恢复,编写一个会触发 panic 的处理函数:

1 2 3 4 5 func panics(w http.ResponseWriter, r *http.Request) { panic("not implemented") } mux.HandleFunc("/panic", panics)

运行,在浏览器中请求localhost:8080/和localhost:8080/greeting,最后请求localhost:8080/panic触发 panic:

思考题

思考题:

这其实就是看阅读代码是不是仔细,最长前缀的排序列表在ServeMux.Handle()方法中生成:

1 2 3 4 5 func (mux *ServeMux) Handle(pattern string, handler Handler) { if pattern[len(pattern)-1] == '/' { mux.es = appendSorted(mux.es, e) } }

这里明显有个限制条件,即注册路径最后必须以/结尾才会触发。所以localhost:8080/greeting/a/b/c和localhost:8080/a/b/c都只会匹配/路径。如果想要让localhost:8080/greeting/a/b/c匹配路径/greeting,注册路径需要改为/greeting/:

1 http.Handle("/greeting/", greeting("Welcome to go web frameworks"))

这时请求路径/greeting会自动重定向(301)到/greeting/。

总结

本文介绍了使用标准库net/http创建 Web 服务器的基本流程,一步步分析源码。然后介绍了如何使用中间件简化通用的处理逻辑。学习并理解了net/http库的内容对于学习其他的 Go Web 框架非常有帮助。第三方的 Go Web 框架大多也是基于net/http实现自己的ServeMux对象而已。

大家如果发现好玩、好用的 Go 语言库,欢迎到 Go 每日一库 GitHub 上提交 issue😄

参考 Go 每日一库 GitHub:https://github.com/darjun/go-daily-lib 我

我的博客:https://darjun.github.io

欢迎关注我的微信公众号【GoUpUp】,共同学习,一起进步~

文章作者 darjun

上次更新 2021-07-13

许可协议 CC BY-NC-ND 4.0



【本文地址】

公司简介

联系我们

今日新闻

    推荐新闻

    专题文章
      CopyRight 2018-2019 实验室设备网 版权所有